/*
 * Copyright (c) 2011 Salzburg Research.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package at.newmedialab.skwrl.reasoner;

import at.newmedialab.skwrl.model.*;
import kiwi.core.api.persistence.PersistenceService;
import kiwi.core.api.query.QueryResult;
import kiwi.core.api.query.QueryService;
import kiwi.core.model.query.*;
import kiwi.core.model.rdf.KiWiNode;
import kiwi.core.model.rdf.KiWiTriple;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import java.util.*;

/**
 * Add file description here!
 * <p/>
 * User: sschaffe
 */
public class QueryEngine<Triple,Node> {

    /**
     * Get the LMF logger for issuing logging statements.
     */
    private Logger log = LoggerFactory.getLogger(QueryEngine.class);


    /**
     * Evaluate a query on the triple store. The query parameters passed to the method call are
     * translated into database queries in HQL and directly evaluated by a single database
     * query that yields the query bindings.
     * <p/>
     * The query patterns may share variable fields, in which case the database query will evaluate
     * a join. In case the optional parameter is true, the database query will succeed even if the query
     * does not yield a value.
     *
     * @param patterns        the set of patterns to query; patterns are considered to be connected by AND;
     *                        occurrences of the same variable in multiple patterns will be evaluated as a join
     * @param initialBindings initial bindings of variable fields may be used in case some bindings are
     *                        already available to further restrict the results; the purpose of this parameter
     *                        is to support query-by-example queries as well as to speed up e.g. incremental
     *                        reasoning
     * @param filters         a set of filters to apply to the result before returning the bindings; depending
     *                        on the kind of filter, filtering will be carried out by the database or in memory
     * @param orderBy         list of variables by whose bindings the result rows should be ordered; variables
     *                        at the beginning of the list take precedence over variables that are further behind
     * @param offset          paging: start with the result given as parameter
     * @param limit           paging: limit the number of rows by the parameter
     * @param optional        allow variables to be unbound; if this parameter is set, at least one variable
     *                        needs to give a result
     * @return a list of bindings matching the query patterns and filters, ordered by the specified
     *         variables and offset and limited by the parameters given
     */
    @Override
    public List<QueryResult<Triple,Node>> query(Collection<Pattern<Node>> patterns, QueryResult<Triple,Node> initialBindings, Set<Filter> filters, List<VariableField<Node>> orderBy, int offset, int limit, boolean optional, boolean justifications) {
        long start = System.currentTimeMillis();

        if(filters != null) {
            throw new IllegalArgumentException("filters are not yet supported by the QueryService");
        }

        // some definitions
        String[] positions = new String[] {"subject","property","object","context"};


        // associate a name with each pattern; the names are used in the database query to refer to the triple
        // that matched this pattern and in the construction of variable names for the HQL query
        int patternCount = 0;
        Map<Pattern<Node>,String> patternNames = new HashMap<Pattern<Node>, String>();
        for(Pattern<Node> p : patterns) {
            patternNames.put(p,"P"+ (++patternCount));
        }

        // find all variables occurring in the patterns and create a map to map them to
        // field names in the database query; each variable will have one or several field names,
        // one for each pattern it occurs in; field names are constructed automatically by a counter
        // and the pattern name to ensure the name is a valid HQL identifier
        int variableCount = 0;

        // a map for the variable names; will look like { ?x -> "V1", ?y -> "V2", ... }
        Map<VariableField,String> variableNames = new HashMap<VariableField, String>();

        // a map for mapping variables to field names; each variable might have one or more field names,
        // depending on the number of patterns it occurs in; will look like
        // { ?x -> ["P1_V1", "P2_V1"}, ?y -> ["P2_V2"], ... }
        Map<VariableField<Node>,List<String>> queryVariables = new HashMap<VariableField<Node>, List<String>>();
        for(Pattern<Node> p : patterns) {
            Field<Node>[] fields = new Field[] {
                    p.getSubject(),
                    p.getProperty(),
                    p.getObject(),
                    p.getContext()
            };
            for(int i = 0; i<fields.length; i++) {
                if(fields[i] != null && fields[i].isVariableField()) {
                    VariableField<Node> v = (VariableField<Node>)fields[i];
                    if(variableNames.get(v) == null) {
                        variableNames.put(v,"V"+ (++variableCount));
                        queryVariables.put(v,new LinkedList<String>());
                    }
                    String pName = patternNames.get(p);
                    String vName = variableNames.get(v);
                    queryVariables.get(v).add(pName + "_" + positions[i] + "_" + vName);
                }
            }
        }

        // build the select clause by projecting for each query variable the first name
        StringBuilder selectClause = new StringBuilder();
        List<VariableField<Node>> selectVariables = new LinkedList<VariableField<Node>>();
        for(Iterator<VariableField<Node>> it = queryVariables.keySet().iterator(); it.hasNext(); ) {
            VariableField v = it.next();
            String projectedName = variableNames.get(v);
            String fromName = queryVariables.get(v).get(0);
            selectClause.append(fromName);
            selectClause.append(" as ");
            selectClause.append(projectedName);
            if(it.hasNext()) {
                selectClause.append(", ");
            }
            selectVariables.add(v);
        }
        if(justifications) {
            // project also the ids of triples that have matched; we use it for building justifications
            for(Iterator<Pattern<Node>> it = patterns.iterator(); it.hasNext(); ) {
                Pattern p = it.next();
                if(selectClause.length() > 0) {
                    selectClause.append(", ");
                }
                selectClause.append(patternNames.get(p));
            }
        }



        // build the from-clause of the query; the from clause is constructed as follows:
        // 1. for each pattern P, there will be a "KiWiTriple P" in the from clause
        // 2. for each variable V in P occurring in
        //    - subject, there will be a "inner join P.subject as P_S_V" or "left outer join P.subject as P_S_V",
        //      depending on whether the "optional" parameter is false or true
        //    - property, there will be a "inner join P.property as P_P_V" or "left outer join p.property as P_P_V"
        //    - object, there will be a "inner join P.object as P_O_V" or "left outer join p.object as P_O_V"
        //    - context, there will be a "inner join P.context as P_C_V" or "left outer join p.context as P_C_V"
        StringBuilder fromClause = new StringBuilder();
        for(Iterator<Pattern> it = patterns.iterator(); it.hasNext(); ) {
            Pattern p = it.next();
            String pName = patternNames.get(p);
            fromClause.append("KiWiTriple "+pName);

            Field[] fields = new Field[] {
                    p.getSubject(),
                    p.getProperty(),
                    p.getObject(),
                    p.getContext()
            };
            for(int i = 0; i<fields.length; i++) {
                if(fields[i] != null && fields[i].isVariableField()) {
                    String vName = variableNames.get(fields[i]);
                    if(optional) {
                        fromClause.append(" left outer join "+pName+"."+positions[i]+" as ");
                    } else {
                        fromClause.append(" inner join "+pName+"."+positions[i]+" as ");
                    }
                    fromClause.append(pName + "_"+positions[i]+"_" + vName);
                }
            }

            if(it.hasNext()) {
                fromClause.append(",\n ");
            }
        }


        // build the where clause as follows:
        // 1. iterate over all patterns and for each resource and literal field in subject,
        //    property, object, or context, and set a query condition according to the
        //    nodes given in the pattern
        // 2. for each variable that has more than one occurrences, add a join condition
        // 3. for each variable in the initialBindings, add a condition to the where clause

        // list of where conditions that will later be connected by AND
        List<String> whereConditions = new LinkedList<String>();


        // 1. iterate over all patterns and for each resource and literal field in subject,
        //    property, object, or context, and set a query condition according to the
        //    nodes given in the pattern
        for(Pattern p : patterns) {
            String pName = patternNames.get(p);
            Field[] fields = new Field[] {
                    p.getSubject(),
                    p.getProperty(),
                    p.getObject(),
                    p.getContext()
            };
            for(int i = 0; i<fields.length; i++) {
                // find node id of the resource or literal field and use it in the where clause
                // in this way we can avoid setting too many query parameters
                Long nodeId = null;
                if(fields[i] != null && fields[i].isLiteralField()) {
                    nodeId = ((LiteralField)fields[i]).getLiteral().getId();
                } else if(fields[i] != null && fields[i].isResourceField()) {
                    nodeId = ((ResourceField)fields[i]).getResource().getId();
                }

                if(nodeId != null) {
                    String condition = pName+"."+positions[i]+".id = " + nodeId;
                    whereConditions.add(condition);
                }
            }
        }

        // 2. for each variable that has more than one occurrences, add a join condition
        for(VariableField v : queryVariables.keySet()) {
            List<String> vNames = queryVariables.get(v);
            for(int i = 1; i < vNames.size(); i++) {
                String vName1 = vNames.get(i-1);
                String vName2 = vNames.get(i);
                whereConditions.add(vName1 + " = " + vName2);
            }
        }

        // 3. for each variable in the initialBindings, add a condition to the where clause setting it
        //    to the node given as binding
        if(initialBindings != null) {
            for(VariableField v : initialBindings.getBindings().keySet()) {
                List<String> vNames = queryVariables.get(v);
                if(vNames != null && vNames.size() > 0) {
                    String vName = vNames.get(0);
                    KiWiNode binding = initialBindings.getBindings().get(v);
                    whereConditions.add(vName+".id = "+binding.getId());
                }
            }
        }

        // 4. for each pattern, ensure that the matched triple is not marked as deleted
        for(Pattern p : patterns) {
            String pName = patternNames.get(p);
            whereConditions.add(pName+".deleted = false");
        }

        // construct the where clause
        StringBuilder whereClause = new StringBuilder();
        for(Iterator<String> it = whereConditions.iterator(); it.hasNext(); ) {
            whereClause.append(it.next());
            whereClause.append("\n ");
            if(it.hasNext()) {
                whereClause.append("AND ");
            }
        }


        // finally, construct the order by clause
        StringBuilder orderByClause = new StringBuilder();
        if(orderBy != null && orderBy.size() > 0) {
            for(Iterator<VariableField> it = orderBy.iterator(); it.hasNext(); ) {
                VariableField v = it.next();
                String vName = variableNames.get(v);
                orderByClause.append(vName);
                if(it.hasNext()) {
                    orderByClause.append(", ");
                }
            }
        }

        // build the query string
        String queryString =
                    "SELECT " + selectClause + "\n " +
                    "FROM " + fromClause + "\n " +
                    "WHERE " + whereClause + "\n " +
                    ((orderBy != null && orderBy.size() > 0)?"ORDER BY "+orderByClause+" ASC\n ":"");

        log.debug("constructed HQL query string {}",queryString);

        // execute the query on the database
        EntityManager em = persistenceService.getEntityManager();
        em.getTransaction().begin();

        Query query = em.createQuery(queryString);
        query.setHint("org.hibernate.cacheable", true);
        if(limit > 0) {
            query.setMaxResults(limit);
        }
        if(offset > 0) {
            query.setFirstResult(0);
        }
        List qResult = query.getResultList();

        em.getTransaction().commit();

        // construct the result in the binding format used by the LMF; each row in the result is a map
        // mapping each variable occurring in the patterns to a KiWiNode
        List<QueryResult> result = new LinkedList<QueryResult>();
        for(Object[] qRow : (List<Object[]>)qResult) {
            QueryResult resultRow = new QueryResult();

            for(int i = 0; i < selectVariables.size(); i++) {
                resultRow.getBindings().put(selectVariables.get(i), (KiWiNode) qRow[i]);
            }

            if(justifications) {
                for(int i = selectVariables.size(); i < selectVariables.size() + patterns.size(); i++) {
                    resultRow.getJustifications().add((KiWiTriple)qRow[i]);
                }
            }

            if(initialBindings != null && initialBindings.getBindings().size() > 0) {
                for(VariableField v : initialBindings.getBindings().keySet()) {
                    if(!resultRow.getBindings().containsKey(v)) {
                        resultRow.getBindings().put(v,initialBindings.getBindings().get(v));
                    }
                }
                if(justifications) {
                    resultRow.getJustifications().addAll(initialBindings.getJustifications());
                }
            }

            result.add(resultRow);
        }


        log.debug("query evaluation in {} ms",System.currentTimeMillis()-start);

        return result;  //To change body of implemented methods use File | Settings | File Templates.
    }


    /**
     * Simplified version of the query method that is useful in many cases. Takes only the query patterns
     * as argument and returns all bindings for the variables occurring in the patterns in arbitrary order.
     *
     * @param patterns the set of patterns to query; patterns are considered to be connected by AND;
     *                 occurrences of the same variable in multiple patterns will be evaluated as a join
     * @return a list of bindings matching the query patternsn
     */
    @Override
    public List<QueryResult> query(Collection<Pattern> patterns) {
        return query(patterns,null,null,null,0,0,true,false);
    }
}
